Pressidian
花园入口
笔记
项目
关于
实验室
GitHub
花园入口
笔记
项目
关于
实验室
GitHub

KNOWLEDGE PATHS

笔记库
当前位置
笔记库/前端/三件套/JS/JavaScript

11 浏览器与DOM

21 分钟阅读 · Note

目录树 578 篇

            • 00 JavaScript学习地图
            • 01 JavaScript基础
            • 02 类型系统与类型转换
            • 03 Number与String
            • 04 Array数组
            • 05 Object对象
            • 06 集合与迭代
            • 07 函数、作用域与this
            • 08 原型、构造函数与类
            • 09 Generator与异步迭代
            • 10 异步编程
            • 11 浏览器与DOM
            • 12 二进制与文件
            • 13 标准内建对象
            • 14 模块化
            • 15 错误处理
            • 16 JSDoc
            • 90 JavaScript知识点总目录
            • 91 JavaScript修正与补充记录
          • 前端模块化
      • 前端技术栈
    • 笔记目录
    • CLAUDE.md
    • Vue 组件与 Render 函数

关联笔记 6

↗事件冒泡、捕获与委托正文关联↗00 JavaScript学习地图反向链接↗90 JavaScript知识点总目录反向链接↗91 JavaScript修正与补充记录反向链接↗跨域&跨站反向链接↗01 JavaScript基础同一路径
  • 11 浏览器与DOM

11 浏览器与DOM

> Last Format Time:8/11/2026 21:45:40

本笔记由两个 JavaScript 教程目录中的同主题内容合并而成;全部旧知识保留在“旧笔记知识全集(按来源保留)”中,并由导航逐项索引。


当前前端开发关键要点

> [!important] 学习优先级:P0 > - 掌握 DOM 查询、创建、更新、删除和事件委托;批量更新时关注重排、重绘与可访问性。 > - 理解 window、location、history、document 等浏览器对象的职责与安全边界。 > - 网络请求优先使用 Fetch 等现代 API,并正确处理 HTTP 状态、CORS、取消、超时和响应解析。 > - 表单处理要兼顾原生校验、受控状态、提交幂等、错误反馈和键盘可用性。


知识点导航

[廖雪峰教程 · 7浏览器/7.1DOM/7.1.1 浏览器对象](./11 浏览器与DOM.md)

  • 浏览器对象
  • window
  • navigator
  • screen
  • location
  • document
  • history

[廖雪峰教程 · 7浏览器/7.1DOM/7.1.2 操作DOM](./11 浏览器与DOM.md)

  • 操作DOM
  • 更新DOM

[廖雪峰教程 · 7浏览器/7.1DOM/7.1.3 插入DOM](./11 浏览器与DOM.md)

  • 插入DOM
  • insertBefore

[廖雪峰教程 · 7浏览器/7.1DOM/7.1.4 删除DOM](./11 浏览器与DOM.md)

  • 删除DOM

[廖雪峰教程 · 7浏览器/7.2 表单](./11 浏览器与DOM.md)

  • 表单
  • 获取值
  • 设置值
  • HTML5控件
  • 提交表单

[廖雪峰教程 · 7浏览器/7.4 AJAX](./11 浏览器与DOM.md)

  • AJAX
  • 安全限制
  • CORS

[廖雪峰教程 · 7浏览器/7.7 Canvas](./11 浏览器与DOM.md)

  • Canvas
  • 绘制形状
  • 绘制文本

现代前端补充与纠错

> [!info] 修改标记 > - 【修正】:旧教程中错误、过时或容易误导的内容。 > - 【补充】:旧教程未覆盖、但当前前端开发需要掌握的内容。 > - 【修正代码】:替换或校正了旧代码示例。

> [!warning] 下方保留 XHR、JSONP、Flash 的历史上下文,过时结论已就地标成 【修正】;现代浏览器请求以 Fetch/CORS 为主。

DOM 与事件

  • 【补充】 事件传播分捕获、目标、冒泡阶段;列表和动态节点优先用事件委托,并结合 closest 与容器边界判断目标。
  • 【补充】 preventDefault 阻止默认行为,stopPropagation 阻止传播,二者不是一回事;被动监听器中不能阻止默认滚动。
  • 【补充】 使用 once、passive、capture 和 signal 明确监听器生命周期;组件销毁时必须释放监听器。详见 [事件冒泡、捕获与委托](../../../浏览器&网络/浏览器环境/Web API/事件/事件冒泡、捕获与委托.md)。
  • 【补充】 DOM 读写交错可能造成强制同步布局;批量修改、CSS class 和合适的动画 API 通常更稳妥。

网络与浏览器 API

  • 【补充】 Fetch 是主流请求入口;始终检查 response.ok,理解请求/响应 body 只能消费一次,并用 AbortSignal 取消。
  • 【修正】 CORS 是基于 HTTP 响应头的跨源访问机制,不应简单称为“HTML5 功能”;JSONP 仅支持 GET 且有脚本注入风险,只用于维护遗留系统。
  • 【补充】 URL 解析使用 URL/URLSearchParams,不要手工拼接查询串。
  • 【补充】 存储、Observer、Worker、Service Worker 等主题应与浏览器环境笔记联读;主线程与 Worker 之间通过结构化克隆/transferable 传递数据。

旧笔记知识全集(按来源保留)

廖雪峰教程 · 7浏览器/7.1DOM/7.1.1 浏览器对象

浏览器对象

JavaScript可以获取浏览器提供的很多对象,并进行操作。

window

window对象不但充当全局作用域,而且表示浏览器窗口。

window对象有innerWidth和innerHeight属性,可以获取浏览器窗口的内部宽度和高度。内部宽高是指除去菜单栏、工具栏、边框等占位元素后,用于显示网页的净宽高。

对应的,还有一个outerWidth和outerHeight属性,可以获取浏览器窗口的整个宽高。

navigator

navigator对象表示浏览器的信息,最常用的属性包括:

  • navigator.appName:浏览器名称;
  • navigator.appVersion:浏览器版本;
  • navigator.language:浏览器设置的语言;
  • navigator.platform:操作系统类型;
  • navigator.userAgent:浏览器设定的User-Agent字符串。

请注意,navigator的信息可以很容易地被用户修改,所以JavaScript读取的值不一定是正确的。很多初学者为了针对不同浏览器编写不同的代码,喜欢用if判断浏览器版本,例如:

let width;
if (getIEVersion(navigator.userAgent) < 9) {
    width = document.body.clientWidth;
} else {
    width = window.innerWidth;
}

但这样既可能判断不准确,也很难维护代码。正确的方法是充分利用JavaScript对不存在属性返回undefined的特性,直接用短路运算符||计算:

let width = window.innerWidth || document.body.clientWidth;
screen

screen对象表示屏幕的信息,常用的属性有:

  • screen.width:屏幕宽度,以像素为单位;
  • screen.height:屏幕高度,以像素为单位;
  • screen.colorDepth:返回颜色位数,如8、16、24。
location

location对象表示当前页面的URL信息。例如,一个完整的URL:

http://www.example.com:8080/path/index.html?a=1&b=2#TOP

可以用location.href获取。要获得URL各个部分的值,可以这么写:

location.protocol; // 'http'
location.host; // 'www.example.com'
location.port; // '8080'
location.pathname; // '/path/index.html'
location.search; // '?a=1&b=2'
location.hash; // 'TOP'

要加载一个新页面,可以调用location.assign()。如果要重新加载当前页面,调用location.reload()方法非常方便。

document

document对象表示当前页面。由于HTML在浏览器中以DOM形式表示为树形结构,document对象就是整个DOM树的根节点。

document的title属性是从HTML文档中的&lt;title&gt;xxx&lt;/title&gt;读取的,但是可以动态改变

要查找DOM树的某个节点,需要从document对象开始查找。最常用的查找是根据ID和Tag Name。

我们先准备HTML数据:

<dl id="drink-menu" style="border:solid 1px #ccc;padding:6px;">
    <dt>摩卡</dt>
    <dd>热摩卡咖啡</dd>
    <dt>酸奶</dt>
    <dd>北京老酸奶</dd>
    <dt>果汁</dt>
    <dd>鲜榨苹果汁</dd>
</dl>

用document对象提供的getElementById()和getElementsByTagName()可以按ID获得一个DOM节点和按Tag名称获得一组DOM节点:

document对象还有一个cookie属性,可以获取当前页面的Cookie。

Cookie是由服务器发送的key-value标示符。因为HTTP协议是无状态的,但是服务器要区分到底是哪个用户发过来的请求,就可以用Cookie来区分。当一个用户成功登录后,服务器发送一个Cookie给浏览器,例如user=ABC123XYZ(加密的字符串)...,此后,浏览器访问该网站时,会在请求头附上这个Cookie,服务器根据Cookie即可区分出用户。

Cookie还可以存储网站的一些设置,例如,页面显示的语言等等。

JavaScript可以通过document.cookie读取到当前页面的Cookie:

document.cookie; // 'v=123; remember=true; prefer=zh'

由于JavaScript能读取到页面的Cookie,而用户的登录信息通常也存在Cookie中,这就造成了巨大的安全隐患,这是因为在HTML页面中引入第三方的JavaScript代码是允许的:

<!-- 当前页面在wwwexample.com -->
<html>
    <head>
        <script src="http://www.foo.com/jquery.js"></script>
    </head>
    ...
</html>

如果引入的第三方的JavaScript中存在恶意代码,则www.foo.com网站将直接获取到www.example.com网站的用户登录信息。

为了解决这个问题,服务器在设置Cookie时可以使用httpOnly,设定了httpOnly的Cookie将不能被JavaScript读取。这个行为由浏览器实现,主流浏览器均支持httpOnly选项,IE从IE6 SP1开始支持。

为了确保安全,服务器端在设置Cookie时,应该始终坚持使用httpOnly。

history

history对象保存了浏览器的历史记录,JavaScript可以调用history对象的back()或forward (),相当于用户点击了浏览器的“后退”或“前进”按钮。

这个对象属于历史遗留对象,对于现代Web页面来说,由于大量使用AJAX和页面交互,简单粗暴地调用history.back()可能会让用户感到非常愤怒。

新手开始设计Web页面时喜欢在登录页登录成功时调用history.back(),试图回到登录前的页面。这是一种错误的方法。

对使用AJAX动态加载的页面,如果希望页面更新时同时更新history对象,应当使用history.pushState()方法:

// when AJAX is done:
let state = 'any-data';
let newUrl = '/ajax.html#signin';
history.pushState(state, '', newUrl);

当用户点击“后退”时,浏览器并不会刷新页面,而是触发popstate事件,可由JavaScript捕获并更新相应的部分页面内容。

廖雪峰教程 · 7浏览器/7.1DOM/7.1.2 操作DOM

操作DOM

由于HTML文档被浏览器解析后就是一棵DOM树,要改变HTML的结构,就需要通过JavaScript来操作DOM。

始终记住DOM是一个树形结构。操作一个DOM节点实际上就是这么几个操作:

  • 更新:更新该DOM节点的内容,相当于更新了该DOM节点表示的HTML的内容;
  • 遍历:遍历该DOM节点下的子节点,以便进行进一步操作;
  • 添加:在该DOM节点下新增一个子节点,相当于动态增加了一个HTML节点;
  • 删除:将该节点从HTML中删除,相当于删掉了该DOM节点的内容以及它包含的所有子节点。

在操作一个DOM节点前,我们需要通过各种方式先拿到这个DOM节点。最常用的方法是document.getElementById()和document.getElementsByTagName(),以及CSS选择器document.getElementsByClassName()。

由于ID在HTML文档中是唯一的,所以document.getElementById()可以直接定位唯一的一个DOM节点。document.getElementsByTagName()和document.getElementsByClassName()总是返回一组DOM节点。要精确地选择DOM,可以先定位父节点,再从父节点开始选择,以缩小范围。

例如:

// 返回ID为'test'的节点:
let test = document.getElementById('test');

// 先定位ID为'test-table'的节点,再返回其内部所有tr节点:
let trs = document.getElementById('test-table').getElementsByTagName('tr');

// 先定位ID为'test-div'的节点,再返回其内部所有class包含red的节点:
let reds = document.getElementById('test-div').getElementsByClassName('red');

// 获取节点test下的所有直属子节点:
let cs = test.children;

// 获取节点test下第一个、最后一个子节点:
let first = test.firstElementChild;
let last = test.lastElementChild;

第二种方法是使用querySelector()和querySelectorAll(),需要了解selector语法,然后使用条件来获取节点,更加方便:

// 通过querySelector获取ID为q1的节点:
let q1 = document.querySelector('#q1');

// 通过querySelectorAll获取q1节点内的符合条件的所有节点:
let ps = q1.querySelectorAll('div.highlighted > p');

注意:低版本的IE<8不支持querySelector和querySelectorAll。IE8仅有限支持。

严格地讲,我们这里的DOM节点是指Element,但是DOM节点实际上是Node,在HTML中,Node包括Element、Comment、CDATA_SECTION等很多种,以及根节点Document类型,但是,绝大多数时候我们只关心Element,也就是实际控制页面结构的Node,其他类型的Node忽略即可。根节点Document已经自动绑定为全局变量document。

更新DOM

拿到一个DOM节点后,我们可以对它进行更新。

可以直接修改节点的文本,方法有两种: 一种是修改innerHTML属性,这个方式非常强大,不但可以修改一个DOM节点的文本内容,还可以直接通过HTML片段修改DOM节点内部的子树:

// 获取<p id="p-id">...</p>
let p = document.getElementById('p-id');
// 设置文本为abc:
p.innerHTML = 'ABC'; // <p id="p-id">ABC</p>
// 设置HTML:
p.innerHTML = 'ABC <span style="color:red">RED</span> XYZ';
// <p>...</p>的内部结构已修改

用innerHTML时要注意,是否需要写入HTML。如果写入的字符串是通过网络拿到的,要注意对字符编码来避免XSS攻击。

第二种是修改innerText或textContent属性,这样可以自动对字符串进行HTML编码,保证无法设置任何HTML标签:

// 获取<p id="p-id">...</p>
let p = document.getElementById('p-id');
// 设置文本:
p.innerText = '<script>alert("Hi")</script>';
// HTML被自动编码,无法设置一个<script>节点:
// <p id="p-id">&lt;script&gt;alert("Hi")&lt;/script&gt;</p>

两者的区别在于读取属性时,innerText不返回隐藏元素的文本,而textContent返回所有文本。另外注意IE<9不支持textContent。

修改CSS也是经常需要的操作。DOM节点的style属性对应所有的CSS,可以直接获取或设置。因为CSS允许font-size这样的名称,但它并非JavaScript有效的属性名,所以需要在JavaScript中改写为驼峰式命名fontSize:

// 获取<p id="p-id">...</p>
let p = document.getElementById('p-id');
// 设置CSS:
p.style.color = '#ff0000';
p.style.fontSize = '20px';
p.style.paddingTop = '2em';

廖雪峰教程 · 7浏览器/7.1DOM/7.1.3 插入DOM

插入DOM

有两个办法可以插入新的节点。一个是使用appendChild,把一个子节点添加到父节点的最后一个子节点。例如:

<!-- HTML结构 -->
<p id="js">JavaScript</p>
<div id="list">
    <p id="java">Java</p>
    <p id="python">Python</p>
    <p id="scheme">Scheme</p>
</div>

把&lt;p id="js"&gt;JavaScript&lt;/p&gt;添加到&lt;div id="list"&gt;的最后一项:

let
    js = document.getElementById('js'),
    list = document.getElementById('list');
list.appendChild(js);

现在,HTML结构变成了这样:

<!-- HTML结构 -->
<div id="list">
    <p id="java">Java</p>
    <p id="python">Python</p>
    <p id="scheme">Scheme</p>
    <p id="js">JavaScript</p>
</div>

因为我们插入的js节点已经存在于当前的文档树,因此这个节点首先会从原先的位置删除,再插入到新的位置。

更多的时候我们会从零创建一个新的节点,然后插入到指定位置:

let
    list = document.getElementById('list'),
    haskell = document.createElement('p');
haskell.id = 'haskell';
haskell.innerText = 'Haskell';
list.appendChild(haskell);

这样我们就动态添加了一个新的节点:

<!-- HTML结构 -->
<div id="list">
    <p id="java">Java</p>
    <p id="python">Python</p>
    <p id="scheme">Scheme</p>
    <p id="haskell">Haskell</p>
</div>

动态创建一个节点然后添加到DOM树中,可以实现很多功能。举个例子,下面的代码动态创建了一个&lt;style&gt;节点,然后把它添加到&lt;head&gt;节点的末尾,这样就动态地给文档添加了新的CSS定义:

let d = document.createElement('style');
d.setAttribute('type', 'text/css');
d.innerHTML = 'p { color: red }';
document.getElementsByTagName('head')[0].appendChild(d);
insertBefore

如果我们要把子节点插入到指定的位置怎么办?可以使用parentElement.insertBefore(newElement, referenceElement);,子节点会插入到referenceElement之前。

还是以上面的HTML为例,假定我们要把Haskell插入到Python之前:

<!-- HTML结构 -->
<div id="list">
    <p id="java">Java</p>
    <p id="python">Python</p>
    <p id="scheme">Scheme</p>
</div>

可以这么写:

let
    list = document.getElementById('list'),
    ref = document.getElementById('python'),
    haskell = document.createElement('p');
haskell.id = 'haskell';
haskell.innerText = 'Haskell';
list.insertBefore(haskell, ref);

新的HTML结构如下:

<!-- HTML结构 -->
<div id="list">
    <p id="java">Java</p>
    <p id="haskell">Haskell</p>
    <p id="python">Python</p>
    <p id="scheme">Scheme</p>
</div>

可见,使用insertBefore重点是要拿到一个“参考子节点”的引用。很多时候,需要循环一个父节点的所有子节点,可以通过迭代children属性实现:

let
    i, c,
    list = document.getElementById('list');
for (i = 0; i < list.children.length; i++) {
    c = list.children[i]; // 拿到第i个子节点
}

廖雪峰教程 · 7浏览器/7.1DOM/7.1.4 删除DOM

删除DOM

删除一个DOM节点就比插入要容易得多。

要删除一个节点,首先要获得该节点本身以及它的父节点,然后,调用父节点的removeChild把自己删掉:

// 拿到待删除节点:
let self = document.getElementById('to-be-removed');
// 拿到父节点:
let parent = self.parentElement;
// 删除:
let removed = parent.removeChild(self);
removed === self; // true

注意到删除后的节点虽然不在文档树中了,但其实它还在内存中,可以随时再次被添加到别的位置。

当你遍历一个父节点的子节点并进行删除操作时,要注意,children属性是一个只读属性,并且它在子节点变化时会实时更新。

例如,对于如下HTML结构:

<div id="parent">
    <p>First</p>
    <p>Second</p>
</div>

当我们用如下代码删除子节点时:

let parent = document.getElementById('parent');
parent.removeChild(parent.children[0]);
parent.removeChild(parent.children[1]); // <-- 浏览器报错

浏览器报错:parent.children[1]不是一个有效的节点。原因就在于,当&lt;p&gt;First&lt;/p&gt;节点被删除后,parent.children的节点数量已经从2变为了1,索引[1]已经不存在了。

因此,删除多个节点时,要注意children属性时刻都在变化。

廖雪峰教程 · 7浏览器/7.2 表单

表单

用JavaScript操作表单和操作DOM是类似的,因为表单本身也是DOM树。

不过表单的输入框、下拉框等可以接收用户输入,所以用JavaScript来操作表单,可以获得用户输入的内容,或者对一个输入框设置新的内容。

HTML表单的输入控件主要有以下几种:

  • 文本框,对应的&lt;input type="text"&gt;,用于输入文本;
  • 口令框,对应的&lt;input type="password"&gt;,用于输入口令;
  • 单选框,对应的&lt;input type="radio"&gt;,用于选择一项;
  • 复选框,对应的&lt;input type="checkbox"&gt;,用于选择多项;
  • 下拉框,对应的&lt;select&gt;,用于选择一项;
  • 隐藏文本,对应的&lt;input type="hidden"&gt;,用户不可见,但表单提交时会把隐藏文本发送到服务器。
获取值

如果我们获得了一个&lt;input&gt;节点的引用,就可以直接调用value获得对应的用户输入值:

// <input type="text" id="email">
let input = document.getElementById('email');
input.value; // '用户输入的值'

这种方式可以应用于text、password、hidden以及select。但是,对于单选框和复选框,value属性返回的永远是HTML预设的值,而我们需要获得的实际是用户是否“勾上了”选项,所以应该用checked判断:

// <label><input type="radio" name="weekday" id="monday" value="1"> Monday</label>
// <label><input type="radio" name="weekday" id="tuesday" value="2"> Tuesday</label>
let mon = document.getElementById('monday');
let tue = document.getElementById('tuesday');
mon.value; // '1'
tue.value; // '2'
mon.checked; // true或者false
tue.checked; // true或者false
设置值

设置值和获取值类似,对于text、password、hidden以及select,直接设置value就可以:

// <input type="text" id="email">
let input = document.getElementById('email');
input.value = 'test@example.com'; // 文本框的内容已更新

对于单选框和复选框,设置checked为true或false即可。

HTML5控件

HTML5新增了大量标准控件,常用的包括date、datetime、datetime-local、color等,它们都使用&lt;input&gt;标签:

<input type="date" value="2021-12-02">
<input type="datetime-local" value="2021-12-02T20:21:12">
<input type="color" value="#ff0000">

不支持HTML5的浏览器无法识别新的控件,会把它们当做type="text"来显示。支持HTML5的浏览器将获得格式化的字符串。例如,type="date"类型的input的value将保证是一个有效的YYYY-MM-DD格式的日期,或者空字符串。

提交表单

最后,JavaScript可以以两种方式来处理表单的提交(AJAX方式在后面章节介绍)。

方式一是通过&lt;form&gt;元素的submit()方法提交一个表单,例如,响应一个&lt;button&gt;的click事件,在JavaScript代码中提交表单:

<!-- HTML -->
<form id="test-form">
    <input type="text" name="test">
    <button type="button" onclick="doSubmitForm()">Submit</button>
</form>

<script>
function doSubmitForm() {
    let form = document.getElementById('test-form');
    // 可以在此修改form的input...
    // 提交form:
    form.submit();
}
</script>

这种方式的缺点是扰乱了浏览器对form的正常提交。浏览器默认点击&lt;button type="submit"&gt;时提交表单,或者用户在最后一个输入框按回车键。因此,第二种方式是响应&lt;form&gt;本身的onsubmit事件,在提交form时作修改:

<!-- HTML -->
<form id="test-form" onsubmit="return checkForm()">
    <input type="text" name="test">
    <button type="submit">Submit</button>
</form>

<script>
function checkForm() {
    let form = document.getElementById('test-form');
    // 可以在此修改form的input...
    // 继续下一步:
    return true;
}
</script>

注意要return true来告诉浏览器继续提交,如果return false,浏览器将不会继续提交form,这种情况通常对应用户输入有误,提示用户错误信息后终止提交form。

在检查和修改&lt;input&gt;时,要充分利用&lt;input type="hidden"&gt;来传递数据。

例如,很多登录表单希望用户输入用户名和口令,但是,安全考虑,提交表单时不传输明文口令,而是口令的MD5。普通JavaScript开发人员会直接修改&lt;input&gt;:

<!-- HTML -->
<form id="login-form" method="post" onsubmit="return checkForm()">
    <input type="text" id="username" name="username">
    <input type="password" id="password" name="password">
    <button type="submit">Submit</button>
</form>

<script>
function checkForm() {
    let pwd = document.getElementById('password');
    // 把用户输入的明文变为MD5:
    pwd.value = toMD5(pwd.value);
    // 继续下一步:
    return true;
}
</script>

这个做法看上去没啥问题,但用户输入了口令提交时,口令框的显示会突然从几个*变成32个*(因为MD5有32个字符)。

要想不改变用户的输入,可以利用&lt;input type="hidden"&gt;实现:

<!-- HTML -->
<form id="login-form" method="post" onsubmit="return checkForm()">
    <input type="text" id="username" name="username">
    <input type="password" id="input-password">
    <input type="hidden" id="md5-password" name="password">
    <button type="submit">Submit</button>
</form>

<script>
function checkForm() {
    let input_pwd = document.getElementById('input-password');
    let md5_pwd = document.getElementById('md5-password');
    // 把用户输入的明文变为MD5:
    md5_pwd.value = toMD5(input_pwd.value);
    // 继续下一步:
    return true;
}
</script>

注意到id为md5-password的&lt;input&gt;标记了name="password",而用户输入的id为input-password的&lt;input&gt;没有name属性。没有name属性的&lt;input&gt;的数据不会被提交。

廖雪峰教程 · 7浏览器/7.4 AJAX

AJAX

AJAX不是JavaScript的规范,它只是一个哥们“发明”的缩写:Asynchronous JavaScript and XML,意思就是用JavaScript执行异步网络请求。

如果仔细观察一个Form的提交,你就会发现,一旦用户点击“Submit”按钮,表单开始提交,浏览器就会刷新页面,然后在新页面里告诉你操作是成功了还是失败了。如果不幸由于网络太慢或者其他原因,就会得到一个404页面。

这就是Web的运作原理:一次HTTP请求对应一个页面。

如果要让用户留在当前页面中,同时发出新的HTTP请求,就必须用JavaScript发送这个新请求,接收到数据后,再用JavaScript更新页面,这样一来,用户就感觉自己仍然停留在当前页面,但是数据却可以不断地更新。

最早大规模使用AJAX的就是Gmail,Gmail的页面在首次加载后,剩下的所有数据都依赖于AJAX来更新。

用JavaScript写一个完整的AJAX代码并不复杂,但是需要注意:AJAX请求是异步执行的,也就是说,要通过回调函数获得响应。

8/11/26 【修正】原内容:现代浏览器 AJAX 主要依靠 XMLHttpRequest;修改点:新代码通常优先 Fetch,XHR 主要用于遗留兼容或其特定能力。 在现代浏览器上写AJAX可以使用XMLHttpRequest对象,如果不考虑早期浏览器的兼容性问题,现代浏览器还提供了原生支持的Fetch API,以Promise方式提供。

安全限制

上面代码的URL使用的是相对路径。如果你把它改为'https://www.sina.com.cn/',再运行,肯定报错。在Chrome的控制台里,还可以看到错误信息。

这是因为浏览器的同源策略导致的。默认情况下,JavaScript在发送AJAX请求时,URL的域名必须和当前页面完全一致。

完全一致的意思是,域名要相同(www.example.com和example.com不同),协议要相同(http和https不同),端口号要相同(http默认是:80端口,它和:8080就不同)。有的浏览器口子松一点,允许端口不同,大多数浏览器都会严格遵守这个限制。

那是不是用JavaScript无法请求外域(就是其他网站)的URL了呢?方法还是有的,大概有这么几种:

8/11/26 【修正】原内容:将 Flash 列作跨域方案;修改点:Flash 已退出 Web 平台,以下只作为历史背景。 一是曾经通过Flash插件发送HTTP请求,这种方式可以绕过浏览器的安全限制,但必须安装Flash,并且跟Flash交互。不过Flash用起来麻烦,而且现在用得也越来越少了。

二是通过在同源域名下架设一个代理服务器来转发,JavaScript负责把请求发送到代理服务器:

'/proxy?url=https://www.sina.com.cn'

代理服务器再把结果返回,这样就遵守了浏览器的同源策略。这种方式麻烦之处在于需要服务器端额外做开发。

8/11/26 【修正】原内容:将 JSONP 列作常规跨域方案;修改点:JSONP 仅用于遗留系统,只有 GET 且存在脚本注入风险。 第三种历史方式称为JSONP,它有个限制,只能用GET请求,并且要求返回JavaScript。这种方式跨域实际上是利用了浏览器允许跨域引用JavaScript资源:

<html>
<head>
    <script src="http://example.com/abc.js"></script>
    ...
</head>
<body>
...
</body>
</html>

JSONP通常以函数调用的形式返回,例如,返回JavaScript内容如下:

foo('data');

这样一来,我们如果在页面中先准备好foo()函数,然后给页面动态加一个&lt;script&gt;节点,相当于动态读取外域的JavaScript资源,最后就等着接收回调了。

CORS

如果浏览器支持HTML5,那么就可以一劳永逸地使用新的跨域策略:CORS了。

8/11/26 【修正】原内容:CORS 是 HTML5 规范;修改点:CORS 是浏览器依据 HTTP 响应头执行的跨源访问机制。 CORS全称 Cross-Origin Resource Sharing,通过 HTTP 响应头声明跨源访问权限。

了解CORS前,我们先搞明白概念:

Origin表示本域,也就是浏览器当前页面的域。当JavaScript向外域(如sina.com)发起请求后,浏览器收到响应后,首先检查Access-Control-Allow-Origin是否包含本域,如果是,则此次跨域请求成功,如果不是,则请求失败,JavaScript将无法获取到响应的任何数据。

用一个图来表示就是:

         GET /res/abc.data
         Host: sina.com
┌──────┐ Origin: http://my.com                       ┌────────┐
│my.com│───────────────────────────────────────────▶│sina.com│
│      │◀───────────────────────────────────────────│        │
└──────┘ HTTP/1.1 200 OK                             └────────┘
         Access-Control-Allow-Origin: http://my.com
         Content-Type: text/xml

         <xml data...>

假设本域是my.com,外域是sina.com,只要响应头Access-Control-Allow-Origin为http://my.com,或者是*,本次请求就可以成功。

可见,跨域能否成功,取决于对方服务器是否愿意给你设置一个正确的Access-Control-Allow-Origin,决定权始终在对方手中。

上面这种跨域请求,称之为“简单请求”。简单请求包括GET、HEAD和POST(POST的Content-Type类型 仅限application/x-www-form-urlencoded、multipart/form-data和text/plain),并且不能出现任何自定义头(例如,X-Custom: 12345),通常能满足90%的需求。

无论你是否需要用JavaScript通过CORS跨域请求资源,你都要了解CORS的原理。最新的浏览器全面支持HTML5。在引用外域资源时,除了JavaScript和CSS外,都要验证CORS。例如,当你引用了某个第三方CDN上的字体文件时:

/* CSS */
@font-face {
  font-family: 'FontAwesome';
  src: url('http://cdn.com/fonts/fontawesome.ttf') format('truetype');
}

如果该CDN服务商未正确设置Access-Control-Allow-Origin,那么浏览器无法加载字体资源。

对于PUT、DELETE以及其他类型如application/json的POST请求,在发送AJAX请求之前,浏览器会先发送一个OPTIONS请求(称为preflighted请求)到这个URL上,询问目标服务器是否接受:

OPTIONS /path/to/resource HTTP/1.1
Host: bar.com
Origin: http://my.com
Access-Control-Request-Method: POST

服务器必须响应并明确指出允许的Method:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://my.com
Access-Control-Allow-Methods: POST, GET, PUT, OPTIONS
Access-Control-Max-Age: 86400

浏览器确认服务器响应的Access-Control-Allow-Methods头确实包含将要发送的AJAX请求的Method,才会继续发送AJAX,否则,抛出一个错误。

由于以POST、PUT方式传送JSON格式的数据在REST中很常见,所以要跨域正确处理POST和PUT请求,服务器端必须正确响应OPTIONS请求。

廖雪峰教程 · 7浏览器/7.7 Canvas

Canvas

Canvas是HTML5新增的组件,它就像一块幕布,可以用JavaScript在上面绘制各种图表、动画等。

没有Canvas的年代,绘图只能借助Flash插件实现,页面不得不用JavaScript和Flash进行交互。有了Canvas,我们就再也不需要Flash了,直接使用JavaScript完成绘制。

一个Canvas定义了一个指定尺寸的矩形框,在这个范围内我们可以随意绘制:

<canvas id="test-canvas" width="300" height="200"></canvas>

由于浏览器对HTML5标准支持不一致,所以,通常在&lt;canvas&gt;内部添加一些说明性HTML代码,如果浏览器支持Canvas,它将忽略&lt;canvas&gt;内部的HTML,如果浏览器不支持Canvas,它将显示&lt;canvas&gt;内部的HTML。在使用Canvas前,用canvas.getContext来测试浏览器是否支持Canvas:

<!-- HTML代码 -->
<canvas id="test-canvas" width="200" heigth="100">
    <p>你的浏览器不支持Canvas</p>
</canvas>

getContext('2d')方法让我们拿到一个CanvasRenderingContext2D对象,所有的绘图操作都需要通过这个对象完成。

let ctx = canvas.getContext('2d');

如果需要绘制3D怎么办?HTML5还有一个WebGL规范,允许在Canvas中绘制3D图形:

gl = canvas.getContext("webgl");

本节我们只专注于绘制2D图形。

绘制形状

我们可以在Canvas上绘制各种形状。在绘制前,我们需要先了解一下Canvas的坐标系统:

 0,0                20
  ┌───────────────────────▶ x
  │                 │
  │
  │                 │
10│─ ─ ─ ─ ─ ─ ─ ─ ─●
  │                  20,10
  ▼
  y

Canvas的坐标以左上角为原点,水平向右为X轴,垂直向下为Y轴,以像素为单位,所以每个点都是非负整数。

绘制文本

绘制文本就是在指定的位置输出文本,可以设置文本的字体、样式、阴影等,与CSS完全一致

Canvas除了能绘制基本的形状和文本,还可以实现动画、缩放、各种滤镜和像素转换等高级操作。如果要实现非常复杂的操作,考虑以下优化方案:

  • 通过创建一个不可见的Canvas来绘图,然后将最终绘制结果复制到页面的可见Canvas中;
  • 尽量使用整数坐标而不是浮点数;
  • 可以创建多个重叠的Canvas绘制不同的层,而不是在一个Canvas中绘制非常复杂的图;
  • 背景图片如果不变可以直接用&lt;img&gt;标签并放到最底层。